{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import argparse\n",
    "import cv2\n",
    "import numpy as np\n",
    "import os"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "parser = argparse.ArgumentParser(description=\"Camera Intrinsic Calibration\")\n",
    "parser.add_argument('-input', '--INPUT_TYPE', default='camera', type=str, help='Input Source: camera/video/image')\n",
    "parser.add_argument('-type', '--CAMERA_TYPE', default='fisheye', type=str, help='Camera Type: fisheye/normal')\n",
    "parser.add_argument('-id', '--CAMERA_ID', default=1, type=int, help='Camera ID')\n",
    "parser.add_argument('-path', '--INPUT_PATH', default='./data/', type=str, help='Input Video/Image Path')\n",
    "parser.add_argument('-video', '--VIDEO_FILE', default='video.mp4', type=str, help='Input Video File Name (eg.: video.mp4)')\n",
    "parser.add_argument('-image', '--IMAGE_FILE', default='img_raw', type=str, help='Input Image File Name Prefix (eg.: img_raw)')\n",
    "parser.add_argument('-mode', '--SELECT_MODE', default='auto', type=str, help='Image Select Mode: auto/manual')\n",
    "parser.add_argument('-fw','--FRAME_WIDTH', default=1280, type=int, help='Camera Frame Width')\n",
    "parser.add_argument('-fh','--FRAME_HEIGHT', default=1024, type=int, help='Camera Frame Height')\n",
    "parser.add_argument('-bw','--BORAD_WIDTH', default=7, type=int, help='Chess Board Width (corners number)')\n",
    "parser.add_argument('-bh','--BORAD_HEIGHT', default=6, type=int, help='Chess Board Height (corners number)')\n",
    "parser.add_argument('-size','--SQUARE_SIZE', default=10, type=int, help='Chess Board Square Size (mm)')\n",
    "parser.add_argument('-num','--CALIB_NUMBER', default=5, type=int, help='Least Required Calibration Frame Number')\n",
    "parser.add_argument('-delay','--FRAME_DELAY', default=12, type=int, help='Capture Image Time Interval (frame number)')\n",
    "parser.add_argument('-subpix','--SUBPIX_REGION', default=5, type=int, help='Corners Subpix Optimization Region')\n",
    "parser.add_argument('-fps','--CAMERA_FPS', default=20, type=int, help='Camera Frame per Second(FPS)')\n",
    "parser.add_argument('-fs', '--FOCAL_SCALE', default=0.5, type=float, help='Camera Undistort Focal Scale')\n",
    "parser.add_argument('-ss', '--SIZE_SCALE', default=1, type=float, help='Camera Undistort Size Scale')\n",
    "parser.add_argument('-store','--STORE_FLAG', default=False, type=bool, help='Store Captured Images (Ture/False)')\n",
    "parser.add_argument('-store_path', '--STORE_PATH', default='./data/', type=str, help='Path to Store Captured Images')\n",
    "parser.add_argument('-crop','--CROP_FLAG', default=False, type=bool, help='Crop Input Video/Image to (fw,fh) (Ture/False)')\n",
    "parser.add_argument('-resize','--RESIZE_FLAG', default=False, type=bool, help='Resize Input Video/Image to (fw,fh) (Ture/False)')\n",
    "args = parser.parse_args([])                 # Jupyter Notebook中直接运行时要加[], py文件则去掉"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# args.INPUT_TYPE = 'image'                  # 输入形式 相机/视频/图像\n",
    "# args.CAMERA_TYPE = 'normal'                # 相机类型 鱼眼/普通\n",
    "# args.CAMERA_ID = 1                         # 相机编号\n",
    "# args.INPUT_PATH = './data/'                # 图片、视频输入路径\n",
    "# args.VIDEO_FILE = 'video.mp4'              # 输入视频文件名(含扩展名)\n",
    "# args.IMAGE_FILE = 'raw'                    # 输入图像文件名前缀\n",
    "# args.SELECT_MODE = 'manual'                # 选择自动/手动模式\n",
    "# args.FRAME_WIDTH = 1280                    # 相机分辨率 帧宽度\n",
    "# args.FRAME_HEIGHT = 720                    # 相机分辨率 帧高度\n",
    "# args.BORAD_WIDTH = 7                       # 棋盘宽度 【内角点数】\n",
    "# args.BORAD_HEIGHT = 6                      # 棋盘高度 【内角点数】\n",
    "# args.SQUARE_SIZE = 10                      # 棋盘格边长 mm\n",
    "# args.CALIB_NUMBER = 10                     # 初始化最小标定图片采样数量\n",
    "# args.FRAME_DELAY = 15                      # 间隔多少帧数采样\n",
    "# args.SUBPIX_REGION = 3                     # 角点坐标亚像素优化时的搜索区域大小(根据图像分辨率调整)\n",
    "# args.STORE_FLAG = True                     # 是否保存抓取的图像\n",
    "# args.STORE_PATH = './data/'                # 保存抓取的图像的路径\n",
    "# args.CROP_FLAG = True                      # 是否将输入视频/图像尺寸裁剪至FRAME_WIDTH和FRAME_HEIGHT的设定值\n",
    "# args.RESIZE_FLAG = True                    # 是否将输入视频/图像尺寸缩放至FRAME_WIDTH和FRAME_HEIGHT的设定值（图像缩放会改变相机焦距）"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 外部调用修改参数\n",
    "def getInCalibArgs():\n",
    "    return args\n",
    "\n",
    "def editInCalibArgs(new_args):\n",
    "    global args\n",
    "    args = new_args"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class CalibData:                             # 标定数据类\n",
    "    def __init__(self):\n",
    "        self.type = None                     # 自定义数据类型\n",
    "        self.camera_mat = None               # 相机内参\n",
    "        self.dist_coeff = None               # 畸变参数\n",
    "        self.rvecs = None                    # 旋转向量\n",
    "        self.tvecs = None                    # 平移向量\n",
    "        self.map1 = None                     # 映射矩阵1\n",
    "        self.map2 = None                     # 映射矩阵2\n",
    "        self.reproj_err = None               # 重投影误差\n",
    "        self.ok = False                      # 数据采集完成标志"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# cv2.fisheye.calibrate ( objectPoints,      # 角点在棋盘中的空间坐标向量\n",
    "#                         imagePoints,       # 角点在图像中的坐标向量\n",
    "#                         image_size,        # 图片大小\n",
    "#                         K,                 # 相机内参矩阵\n",
    "#                         D,                 # 畸变参数向量\n",
    "#                         rvecs,             # 旋转向量\n",
    "#                         tvecs,             # 平移向量\n",
    "#                         flags,             # 操作标志\n",
    "#                         criteria           # 迭代优化算法的停止标准\n",
    "#                         )\n",
    "\n",
    "# flags:\n",
    "#     cv2.fisheye.CALIB_USE_INTRINSIC_GUESS     # 当相机内参矩阵包含有效的fx，fy，cx，cy初始值时，这些值会进一步进行优化\n",
    "#                                               # 否则，（cx，cy）初始化设置为图像中心（使用imageSize），并且以最小二乘法计算焦距。\n",
    "#     cv2.fisheye.CALIB_RECOMPUTE_EXTRINSIC     # 在每次内部参数优化迭代之后，将重新计算外部参数。\n",
    "#     cv2.fisheye.CALIB_CHECK_COND              # 检查条件编号的有效性\n",
    "#     cv2.fisheye.CALIB_FIX_SKEW                # 偏斜系数（alpha）设置为零，并保持为零\n",
    "#     cv2.fisheye.CALIB_FIX_K1 (K1-K4)          # 选定的畸变系数设置为零，并保持为零  CALIB_FIX_INTRINSIC则全为零\n",
    "\n",
    "# criteria:\n",
    "#     TermCriteria (int type, int maxCount, double epsilon)      # 类型、最大计数次数、最小精度\n",
    "#     Criteria type, can be one of: COUNT, EPS or COUNT + EPS    # 角点优化最大迭代次数或角点优化位置移动量小于epsilon值"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# cv2.checkRange 检查元素非空及无异常值"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "code_folding": []
   },
   "outputs": [],
   "source": [
    "class Fisheye:           # 鱼眼相机\n",
    "    def __init__(self):\n",
    "        self.data = CalibData()\n",
    "        self.inited = False\n",
    "        self.BOARD = np.array([ [(j * args.SQUARE_SIZE, i * args.SQUARE_SIZE, 0.)]\n",
    "                               for i in range(args.BORAD_HEIGHT) \n",
    "                               for j in range(args.BORAD_WIDTH) ],dtype=np.float32)     # 棋盘角点二维坐标（乘上尺寸）\n",
    "        \n",
    "    # 更新标定数据，分为初始化和精调\n",
    "    def update(self, corners, frame_size):\n",
    "        board = [self.BOARD] * len(corners)\n",
    "        if not self.inited:\n",
    "            self._update_init(board, corners, frame_size)\n",
    "            self.inited = True\n",
    "        else:\n",
    "            self._update_refine(board, corners, frame_size)\n",
    "        self._calc_reproj_err(corners)\n",
    "        self._get_undistort_maps()\n",
    "    \n",
    "    # 得到一定数量标定样本时进行初始标定\n",
    "    def _update_init(self, board, corners, frame_size):\n",
    "        data = self.data\n",
    "        data.type = \"FISHEYE\"\n",
    "        data.camera_mat = np.eye(3, 3)\n",
    "        data.dist_coeff = np.zeros((4, 1))\n",
    "        data.ok, data.camera_mat, data.dist_coeff, data.rvecs, data.tvecs = cv2.fisheye.calibrate(\n",
    "            board, corners, frame_size, data.camera_mat, data.dist_coeff,\n",
    "            flags=cv2.fisheye.CALIB_FIX_SKEW|cv2.fisheye.CALIB_RECOMPUTE_EXTRINSIC,\n",
    "            criteria=(cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_COUNT, 30, 1e-6)) \n",
    "        data.ok = data.ok and cv2.checkRange(data.camera_mat) and cv2.checkRange(data.dist_coeff)\n",
    "\n",
    "    # 精调时启用CALIB_USE_INTRINSIC_GUESS\n",
    "    def _update_refine(self, board, corners, frame_size):\n",
    "        data = self.data\n",
    "        data.ok, data.camera_mat, data.dist_coeff, data.rvecs, data.tvecs = cv2.fisheye.calibrate(\n",
    "            board, corners, frame_size, data.camera_mat, data.dist_coeff,\n",
    "            flags=cv2.fisheye.CALIB_FIX_SKEW|cv2.fisheye.CALIB_RECOMPUTE_EXTRINSIC|cv2.CALIB_USE_INTRINSIC_GUESS,\n",
    "            criteria=(cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_COUNT, 10, 1e-6))\n",
    "        data.ok = data.ok and cv2.checkRange(data.camera_mat) and cv2.checkRange(data.dist_coeff)\n",
    "\n",
    "    # 计算重投影误差，单位为像素\n",
    "    def _calc_reproj_err(self, corners):\n",
    "        if not self.inited: return\n",
    "        data = self.data\n",
    "        data.reproj_err = []\n",
    "        for i in range(len(corners)):\n",
    "            corners_reproj, _ = cv2.fisheye.projectPoints(self.BOARD, data.rvecs[i], data.tvecs[i], data.camera_mat, data.dist_coeff)\n",
    "            err = cv2.norm(corners_reproj, corners[i], cv2.NORM_L2) / len(corners_reproj)\n",
    "            data.reproj_err.append(err)\n",
    "            \n",
    "    # 计算去畸变的新的相机内参，可以改变焦距和画幅\n",
    "    def _get_camera_mat_dst(self, camera_mat):\n",
    "        camera_mat_dst = camera_mat.copy()\n",
    "        camera_mat_dst[0][0] *= args.FOCAL_SCALE\n",
    "        camera_mat_dst[1][1] *= args.FOCAL_SCALE\n",
    "        camera_mat_dst[0][2] = args.FRAME_WIDTH / 2 * args.SIZE_SCALE\n",
    "        camera_mat_dst[1][2] = args.FRAME_HEIGHT / 2 * args.SIZE_SCALE\n",
    "        return camera_mat_dst\n",
    "    \n",
    "    # 计算去畸变的映射矩阵\n",
    "    def _get_undistort_maps(self):\n",
    "        data = self.data\n",
    "        camera_mat_dst = self._get_camera_mat_dst(data.camera_mat)\n",
    "        data.map1, data.map2 = cv2.fisheye.initUndistortRectifyMap(\n",
    "                                 data.camera_mat, data.dist_coeff, np.eye(3, 3), camera_mat_dst, \n",
    "                                 (int(args.FRAME_WIDTH * args.SIZE_SCALE), int(args.FRAME_HEIGHT * args.SIZE_SCALE)), cv2.CV_16SC2)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# cv2.calibrateCamera (objectPoints,         # 角点在棋盘中的空间坐标向量        \n",
    "#                      imagePoints,          # 角点在图像中的坐标向量\n",
    "#                      image_size,           # 图片大小\n",
    "#                      K,                    # 相机内参矩阵\n",
    "#                      D,                    # 畸变参数向量\n",
    "#                      rvecs,                # 旋转向量\n",
    "#                      tvecs,                # 平移向量\n",
    "#                      flags,                # 操作标志\n",
    "#                      criteria              # 迭代优化算法的停止标准\n",
    "#                     )\n",
    "\n",
    "# flags:\n",
    "#     cv2.CALIB_USE_INTRINSIC_GUESS          # 当相机内参矩阵包含有效的fx，fy，cx，cy初始值时，这些值会进一步进行优化\n",
    "#                                            # 否则，（cx，cy）初始化设置为图像中心（使用imageSize），并且以最小二乘法计算焦距\n",
    "#     cv2.CALIB_FIX_PRINCIPAL_POINT          # 固定光轴点（当设置CALIB_USE_INTRINSIC_GUESS时可以使用）\n",
    "#     cv2.CALIB_FIX_ASPECT_RATIO             # 固定fx/fy的值，函数仅将fy视为自由参数\n",
    "#     cv2.CALIB_ZERO_TANGENT_DIST            # 切向畸变系数（p1，p2） 设置为零并保持为零\n",
    "#     cv2.CALIB_FIX_FOCAL_LENGTH             # 如果设置了CALIB_USE_INTRINSIC_GUESS，则在全局优化过程中不会更改焦距\n",
    "#     cv2.CALIB_FIX_K1 (K1-K6)               # 固定相应的径向畸变系数为0或给定的初始值\n",
    "#     cv2.CALIB_RATIONAL_MODEL               # 理想模型：启用系数k4，k5和k6。此时返回8个或更多的系数\n",
    "#     cv2.CALIB_THIN_PRISM_MODEL             # 薄棱镜模型：启用系数s1，s2，s3和s4。此时返回12个或更多的系数\n",
    "#     cv2.CALIB_FIX_S1_S2_S3_S4              # 固定薄棱镜畸变系数为0或给定的初始值\n",
    "#     cv2.CALIB_TILTED_MODEL                 # 倾斜模型：启用系数tauX和tauY。此时返回14个系数\n",
    "#     cv2.CALIB_FIX_TAUX_TAUY                # 固定倾斜传感器模型的系数为0或给定的初始值"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Normal:           # 平面相机\n",
    "    def __init__(self):\n",
    "        self.data = CalibData()\n",
    "        self.inited = False\n",
    "        self.BOARD = np.array([ [(j * args.SQUARE_SIZE, i * args.SQUARE_SIZE, 0.)]\n",
    "                               for i in range(args.BORAD_HEIGHT) \n",
    "                               for j in range(args.BORAD_WIDTH) ],dtype=np.float32)\n",
    "        \n",
    "    def update(self, corners, frame_size):\n",
    "        board = [self.BOARD] * len(corners)\n",
    "        if not self.inited:\n",
    "            self._update_init(board, corners, frame_size)\n",
    "            self.inited = True\n",
    "        else:\n",
    "            self._update_refine(board, corners, frame_size)\n",
    "        self._calc_reproj_err(corners)\n",
    "        self._get_undistort_maps()\n",
    "        \n",
    "    def _update_init(self, board, corners, frame_size):\n",
    "        data = self.data\n",
    "        data.type = \"NORMAL\"\n",
    "        data.camera_mat = np.eye(3, 3)\n",
    "        data.dist_coeff = np.zeros((5, 1))     # 畸变向量的尺寸根据使用模型修改\n",
    "        data.ok, data.camera_mat, data.dist_coeff, data.rvecs, data.tvecs = cv2.calibrateCamera(\n",
    "            board, corners, frame_size, data.camera_mat, data.dist_coeff, \n",
    "            criteria=(cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_COUNT, 30, 1e-6))\n",
    "        data.ok = data.ok and cv2.checkRange(data.camera_mat) and cv2.checkRange(data.dist_coeff)\n",
    "        \n",
    "    def _update_refine(self, board, corners, frame_size):\n",
    "        data = self.data\n",
    "        data.ok, data.camera_mat, data.dist_coeff, data.rvecs, data.tvecs = cv2.calibrateCamera(\n",
    "            board, corners, frame_size, data.camera_mat, data.dist_coeff,  \n",
    "            flags = cv2.CALIB_USE_INTRINSIC_GUESS,\n",
    "            criteria=(cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_COUNT, 10, 1e-6))\n",
    "        data.ok = data.ok and cv2.checkRange(data.camera_mat) and cv2.checkRange(data.dist_coeff)\n",
    "        \n",
    "    def _calc_reproj_err(self, corners):\n",
    "        if not self.inited: return\n",
    "        data = self.data\n",
    "        data.reproj_err = []\n",
    "        for i in range(len(corners)):\n",
    "            corners_reproj, _ = cv2.projectPoints(self.BOARD, data.rvecs[i], data.tvecs[i], data.camera_mat, data.dist_coeff)\n",
    "            err = cv2.norm(corners_reproj, corners[i], cv2.NORM_L2) / len(corners_reproj)\n",
    "            data.reproj_err.append(err)\n",
    "            \n",
    "    def _get_camera_mat_dst(self, camera_mat):\n",
    "        camera_mat_dst = camera_mat.copy()\n",
    "        camera_mat_dst[0][0] *= args.FOCAL_SCALE\n",
    "        camera_mat_dst[1][1] *= args.FOCAL_SCALE\n",
    "        camera_mat_dst[0][2] = args.FRAME_WIDTH / 2 * args.SIZE_SCALE\n",
    "        camera_mat_dst[1][2] = args.FRAME_HEIGHT / 2 * args.SIZE_SCALE\n",
    "        return camera_mat_dst\n",
    "    \n",
    "    def _get_undistort_maps(self):\n",
    "        data = self.data\n",
    "        camera_mat_dst = self._get_camera_mat_dst(data.camera_mat)\n",
    "        data.map1, data.map2 = cv2.initUndistortRectifyMap(\n",
    "                                 data.camera_mat, data.dist_coeff, np.eye(3, 3), camera_mat_dst, \n",
    "                                 (int(args.FRAME_WIDTH * args.SIZE_SCALE), int(args.FRAME_HEIGHT * args.SIZE_SCALE)), cv2.CV_16SC2)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# cv2.findChessboardCorners ( image,         # 棋盘图像\n",
    "#                             patternSize,   # 棋盘格行和列的【内角点】数量\n",
    "#                             corners,       # 输出数组\n",
    "#                             flags          # 操作标志\n",
    "#                             )\n",
    "# flags:\n",
    "#     CV_CALIB_CB_ADAPTIVE_THRESH            # 使用自适应阈值处理将图像转换为黑白图像\n",
    "#     CV_CALIB_CB_NORMALIZE_IMAGE            # 对图像进行归一化。\n",
    "#     CV_CALIB_CB_FILTER_QUADS               # 过滤在轮廓检索阶段提取的假四边形。\n",
    "#     CALIB_CB_FAST_CHECK                    # 对查找棋盘角的图像进行快速检查"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# cv2.cornerSubPix (image,                        # 棋盘图像\n",
    "#                   corners,                      # 棋盘角点\n",
    "#                   winSize,                      # 搜索窗口边长的一半\n",
    "#                   zeroZone,                     # 搜索区域死区大小的一半, (-1,-1)代表无\n",
    "#                   criteria                      # 迭代停止标准\n",
    "#                  )"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# cv2.fisheye.initUndistortRectifyMap (K,         # 相机内参矩阵\n",
    "#                                      D,         # 畸变向量\n",
    "#                                      R,         # 旋转矩阵\n",
    "#                                      P,         # 新的相机矩阵\n",
    "#                                      size,      # 输出图像大小\n",
    "#                                      m1type,    # 映射矩阵类型\n",
    "#                                      map1,      # 输出映射矩阵1\n",
    "#                                      map2       # 输出映射矩阵2\n",
    "#                                     )"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class InCalibrator:                  # 内参标定器\n",
    "    def __init__(self, camera):\n",
    "        if camera == 'fisheye':\n",
    "            self.camera = Fisheye()  # 鱼眼相机类\n",
    "        elif camera == 'normal':\n",
    "            self.camera = Normal()   # 普通相机类\n",
    "        else:\n",
    "            raise Exception(\"camera should be fisheye/normal\")\n",
    "        self.corners = []\n",
    "    \n",
    "    # 获取args参数，供外部调用修改参数\n",
    "    @staticmethod\n",
    "    def get_args():\n",
    "        return args\n",
    "    \n",
    "    # 获取棋盘格角点坐标\n",
    "    def get_corners(self, img):\n",
    "        ok, corners = cv2.findChessboardCorners(img, (args.BORAD_WIDTH, args.BORAD_HEIGHT),\n",
    "                      flags = cv2.CALIB_CB_ADAPTIVE_THRESH|cv2.CALIB_CB_NORMALIZE_IMAGE|cv2.CALIB_CB_FAST_CHECK)\n",
    "        if ok: \n",
    "            gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)\n",
    "            # 角点坐标亚像素优化\n",
    "            corners = cv2.cornerSubPix(gray, corners, (args.SUBPIX_REGION, args.SUBPIX_REGION), (-1, -1),\n",
    "                                       (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.01))\n",
    "        return ok, corners\n",
    "    \n",
    "    # 在图上绘制棋盘格角点\n",
    "    def draw_corners(self, img):\n",
    "        ok, corners = self.get_corners(img)\n",
    "        cv2.drawChessboardCorners(img, (args.BORAD_WIDTH, args.BORAD_HEIGHT), corners, ok)\n",
    "        return img\n",
    "    \n",
    "    # 图像去畸变\n",
    "    def undistort(self, img):\n",
    "        data = self.camera.data\n",
    "        return cv2.remap(img, data.map1, data.map2, cv2.INTER_LINEAR)\n",
    "    \n",
    "    # 使用现有角点坐标标定\n",
    "    def calibrate(self, img):\n",
    "        if len(self.corners) >= args.CALIB_NUMBER:\n",
    "            self.camera.update(self.corners, img.shape[1::-1])  # 更新标定数据\n",
    "        return self.camera.data\n",
    "    \n",
    "    def __call__(self, raw_frame):\n",
    "        ok, corners = self.get_corners(raw_frame)\n",
    "        result = self.camera.data\n",
    "        if ok:\n",
    "            self.corners.append(corners)          # 加入新的角点坐标\n",
    "            result = self.calibrate(raw_frame)    # 得到标定结果\n",
    "        return result"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 居中裁剪\n",
    "def centerCrop(img,width,height):\n",
    "    if img.shape[1] < width or img.shape[0] < height:\n",
    "        raise Exception(\"CROP size should be smaller than original size\")\n",
    "    img = img[round((img.shape[0]-height)/2) : round((img.shape[0]-height)/2)+height,\n",
    "              round((img.shape[1]-width)/2) : round((img.shape[1]-width)/2)+width ]  \n",
    "    return img"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 筛选图片文件\n",
    "def get_images(PATH, NAME):\n",
    "    filePath = [os.path.join(PATH, x) for x in os.listdir(PATH) \n",
    "                if any(x.endswith(extension) for extension in ['.png', '.jpg', '.jpeg', '.PNG', '.JPG', '.JPEG'])\n",
    "               ]                                                            # 得到给定路径下所有图片文件\n",
    "    filenames = [filename for filename in filePath if NAME in filename]     # 再筛选出包含给定名字的图片\n",
    "    if len(filenames) == 0:\n",
    "        raise Exception(\"from {} read images failed\".format(PATH))\n",
    "    return filenames"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class CalibMode():\n",
    "    def __init__(self, calibrator, input_type, mode):\n",
    "        self.calibrator = calibrator\n",
    "        self.input_type = input_type\n",
    "        self.mode = mode\n",
    "    \n",
    "    # 图片预处理\n",
    "    def imgPreprocess(self, img):\n",
    "        if args.CROP_FLAG:          # 裁剪图片尺寸\n",
    "            img = centerCrop(img, args.FRAME_WIDTH, args.FRAME_HEIGHT)\n",
    "        elif args.RESIZE_FLAG:      # 缩放图片尺寸\n",
    "            img = cv2.resize(img, (args.FRAME_WIDTH, args.FRAME_HEIGHT))\n",
    "        return img\n",
    "    \n",
    "    # 设置相机\n",
    "    def setCamera(self, cap):\n",
    "        cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter.fourcc('M','J','P','G'))    # 设置编码格式为MJPG\n",
    "        cap.set(cv2.CAP_PROP_FRAME_WIDTH, args.FRAME_WIDTH)                      # 设置相机分辨率\n",
    "        cap.set(cv2.CAP_PROP_FRAME_HEIGHT, args.FRAME_HEIGHT)\n",
    "        cap.set(cv2.CAP_PROP_FPS, args.CAMERA_FPS)                               # 设置相机帧率\n",
    "        return cap\n",
    "    \n",
    "    # 运行标定程序\n",
    "    def runCalib(self, raw_frame, display_raw=True, display_undist=True):\n",
    "        calibrator = self.calibrator\n",
    "        raw_frame = self.imgPreprocess(raw_frame)\n",
    "        result = calibrator(raw_frame)\n",
    "        raw_frame = calibrator.draw_corners(raw_frame)\n",
    "        if display_raw:\n",
    "            cv2.namedWindow(\"raw_frame\", flags = cv2.WINDOW_NORMAL | cv2.WINDOW_KEEPRATIO)\n",
    "            cv2.imshow(\"raw_frame\", raw_frame)\n",
    "        if len(calibrator.corners) > args.CALIB_NUMBER and display_undist: \n",
    "            undist_frame = calibrator.undistort(raw_frame)\n",
    "            cv2.namedWindow(\"undist_frame\", flags = cv2.WINDOW_NORMAL | cv2.WINDOW_KEEPRATIO)\n",
    "            cv2.imshow(\"undist_frame\", undist_frame)   \n",
    "        cv2.waitKey(1)\n",
    "        return result\n",
    "    \n",
    "    # 图片输入自动标定\n",
    "    def imageAutoMode(self):\n",
    "        calibrator = self.calibrator\n",
    "        filenames = get_images(args.INPUT_PATH, args.IMAGE_FILE)\n",
    "        for filename in filenames:\n",
    "            print(filename)\n",
    "            raw_frame = cv2.imread(filename)\n",
    "            result = self.runCalib(raw_frame)\n",
    "            key = cv2.waitKey(1)\n",
    "            if key == 27: break\n",
    "        cv2.destroyAllWindows() \n",
    "        return result\n",
    "    \n",
    "    # 图片输入手动挑选 按空格键确认 其他键丢弃该图片\n",
    "    def imageManualMode(self):\n",
    "        filenames = get_images(args.INPUT_PATH, args.IMAGE_FILE)\n",
    "        for filename in filenames:\n",
    "            print(filename)\n",
    "            raw_frame = cv2.imread(filename)\n",
    "            raw_frame = self.imgPreprocess(raw_frame)\n",
    "            img = raw_frame.copy()\n",
    "            img = self.calibrator.draw_corners(img)\n",
    "            display = \"raw_frame: press SPACE to SELECT, other key to SKIP, press ESC to QUIT\"\n",
    "            cv2.namedWindow(display, flags = cv2.WINDOW_NORMAL | cv2.WINDOW_KEEPRATIO)\n",
    "            cv2.imshow(display, img)\n",
    "            key = cv2.waitKey(0)\n",
    "            if key == 32:\n",
    "                result = self.runCalib(raw_frame, display_raw = False)\n",
    "            if key == 27: break\n",
    "        cv2.destroyAllWindows() \n",
    "        return result\n",
    "    \n",
    "    # 视频输入自动标定\n",
    "    def videoAutoMode(self):\n",
    "        cap = cv2.VideoCapture(args.INPUT_PATH + args.VIDEO_FILE)\n",
    "        if not cap.isOpened(): \n",
    "            raise Exception(\"from {} read video failed\".format(args.INPUT_PATH + args.VIDEO_FILE))\n",
    "        frame_id = 0\n",
    "        while True:\n",
    "            key = cv2.waitKey(1)\n",
    "            ok, raw_frame = cap.read()\n",
    "            raw_frame = self.imgPreprocess(raw_frame)\n",
    "            if frame_id % args.FRAME_DELAY == 0:\n",
    "                if args.STORE_FLAG:  # 存储该帧图像\n",
    "                    cv2.imwrite(args.STORE_PATH + 'img_raw{}.jpg'.format(len(self.calibrator.corners)), raw_frame)\n",
    "                result = self.runCalib(raw_frame) \n",
    "                print(len(self.calibrator.corners))\n",
    "            frame_id += 1 \n",
    "            key = cv2.waitKey(1)\n",
    "            if key == 27: break\n",
    "        cap.release()\n",
    "        cv2.destroyAllWindows() \n",
    "        return result\n",
    "    \n",
    "    # 视频输入手动挑选 按空格键采集图片\n",
    "    def videoManualMode(self):\n",
    "        cap = cv2.VideoCapture(args.INPUT_PATH + args.VIDEO_FILE)\n",
    "        if not cap.isOpened(): \n",
    "            raise Exception(\"from {} read video failed\".format(args.INPUT_PATH + args.VIDEO_FILE))\n",
    "        while True:\n",
    "            key = cv2.waitKey(1)\n",
    "            ok, raw_frame = cap.read()\n",
    "            raw_frame = self.imgPreprocess(raw_frame)\n",
    "            display = \"raw_frame: press SPACE to capture image\"\n",
    "            cv2.namedWindow(display, flags = cv2.WINDOW_NORMAL | cv2.WINDOW_KEEPRATIO)\n",
    "            cv2.imshow(display, raw_frame)\n",
    "            if key == 32:\n",
    "                if args.STORE_FLAG:  # 存储该帧图像\n",
    "                    cv2.imwrite(args.STORE_PATH + 'img_raw{}.jpg'.format(len(self.calibrator.corners)), raw_frame)\n",
    "                result = self.runCalib(raw_frame) \n",
    "                print(len(self.calibrator.corners))\n",
    "            if key == 27: break\n",
    "        cap.release()\n",
    "        cv2.destroyAllWindows() \n",
    "        return result\n",
    "    \n",
    "    # 相机输入在线标定\n",
    "    def cameraAutoMode(self):\n",
    "        cap = cv2.VideoCapture(args.CAMERA_ID)\n",
    "        if not cap.isOpened(): \n",
    "            raise Exception(\"from {} read video failed\".format(args.CAMERA_ID))\n",
    "        cap = self.setCamera(cap)\n",
    "        frame_id = 0\n",
    "        start_flag = False\n",
    "        while True:\n",
    "            key = cv2.waitKey(1)\n",
    "            ok, raw_frame = cap.read()\n",
    "            raw_frame = self.imgPreprocess(raw_frame)\n",
    "            if key == 32: start_flag = True\n",
    "            if key == 27: break\n",
    "            if not start_flag:\n",
    "                cv2.putText(raw_frame, 'press SPACE to start!', (args.FRAME_WIDTH//4,args.FRAME_HEIGHT//2), \n",
    "                             cv2.FONT_HERSHEY_COMPLEX, 1.5, (0,0,255), 2)\n",
    "                cv2.imshow(\"raw_frame\", raw_frame)\n",
    "                continue\n",
    "            if frame_id % args.FRAME_DELAY == 0:\n",
    "                if args.STORE_FLAG:  # 存储该帧图像\n",
    "                    cv2.imwrite(args.STORE_PATH + 'img_raw{}.jpg'.format(len(self.calibrator.corners)), raw_frame)\n",
    "                result = self.runCalib(raw_frame) \n",
    "                print(len(self.calibrator.corners))\n",
    "            frame_id += 1 \n",
    "        cap.release()\n",
    "        cv2.destroyAllWindows() \n",
    "        return result\n",
    "    \n",
    "    # 相机输入手动挑选 按空格键采集图片\n",
    "    def cameraManualMode(self):\n",
    "        cap = cv2.VideoCapture(args.CAMERA_ID)\n",
    "        if not cap.isOpened(): \n",
    "            raise Exception(\"from {} read video failed\".format(args.CAMERA_ID))\n",
    "        cap = self.setCamera(cap)\n",
    "        frame_id = 0\n",
    "        while True:\n",
    "            key = cv2.waitKey(1)\n",
    "            ok, raw_frame = cap.read()\n",
    "            raw_frame = self.imgPreprocess(raw_frame)\n",
    "            display = \"raw_frame: press SPACE to capture image\"\n",
    "            cv2.namedWindow(display, flags = cv2.WINDOW_NORMAL | cv2.WINDOW_KEEPRATIO)\n",
    "            cv2.imshow(display, raw_frame)\n",
    "            if key == 32:\n",
    "                if args.STORE_FLAG:  # 存储该帧图像\n",
    "                    cv2.imwrite(args.STORE_PATH + 'img_raw{}.jpg'.format(len(self.calibrator.corners)), raw_frame)\n",
    "                result = self.runCalib(raw_frame) \n",
    "                print(len(self.calibrator.corners))\n",
    "            if key == 27: break\n",
    "        cap.release()\n",
    "        cv2.destroyAllWindows() \n",
    "        return result\n",
    "\n",
    "    def __call__(self):\n",
    "        input_type = self.input_type\n",
    "        mode = self.mode\n",
    "        if input_type == 'image' and mode == 'auto':\n",
    "            result = self.imageAutoMode()\n",
    "        if input_type == 'image' and mode == 'manual':\n",
    "            result = self.imageManualMode()\n",
    "        if input_type == 'video' and mode == 'auto':\n",
    "            result = self.videoAutoMode()\n",
    "        if input_type == 'video' and mode == 'manual':\n",
    "            result = self.videoManualMode()\n",
    "        if input_type == 'camera' and mode == 'auto':\n",
    "            result = self.cameraAutoMode()\n",
    "        if input_type == 'camera' and mode == 'manual':\n",
    "            result = self.cameraManualMode()\n",
    "        return result"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def main():\n",
    "    calibrator = InCalibrator(args.CAMERA_TYPE)                            # 初始化内参标定器\n",
    "    calib = CalibMode(calibrator, args.INPUT_TYPE, args.SELECT_MODE)       # 选择标定模式\n",
    "    result = calib()                                                       # 开始标定\n",
    "                  \n",
    "    if len(calibrator.corners) == 0:                      # 标定失败 未找到棋盘或参数设置错误\n",
    "        raise Exception(\"Calibration failed. Chessboard not found, check the parameters\")  \n",
    "    if len(calibrator.corners) < args.CALIB_NUMBER:       # 标定样本小于初始化标定所需的图片数\n",
    "        raise Exception(\"Warning: Calibration images are not enough. At least {} valid images are needed.\".format(args.CALIB_NUMBER))            \n",
    "\n",
    "    print(\"Calibration Complete\")\n",
    "    print(\"Camera Matrix is : {}\".format(result.camera_mat.tolist()))                 # 相机内参\n",
    "    print(\"Distortion Coefficient is : {}\".format(result.dist_coeff.tolist()))        # 畸变向量\n",
    "    print(\"Reprojection Error is : {}\".format(np.mean(result.reproj_err)))            # 平均重投影误差\n",
    "    np.save('camera_{}_K.npy'.format(args.CAMERA_ID),result.camera_mat.tolist())\n",
    "    np.save('camera_{}_D.npy'.format(args.CAMERA_ID),result.dist_coeff.tolist())      # 输出并存储数据\n",
    "        \n",
    "if __name__ == '__main__':\n",
    "    main()\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.7.6"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
